﻿// <copyright file="AsyncBatchProcessActionHandler.cs" company="Dynamize">
// Copyright (c) 2015 All Rights Reserved
// </copyright>
// <author>Yaniv Arditi</author>
// <date>7/10/2015</date>
// <summary>Implements the AsyncBatchProcessActionHandler Custom Workflow Activity</summary>

namespace Dynamize.AsyncBatchProcess.ActionHandler
{
    using System;
    using System.Activities;
    using System.Linq;
    using System.ServiceModel;
    using System.Text;
    using System.Text.RegularExpressions;
    using Microsoft.Crm.Sdk.Messages;
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Query;
    using Microsoft.Xrm.Sdk.Workflow;

    /// <summary>
    /// Handle Batch Process Action activation 
    /// </summary>
    public sealed class AsyncBatchProcessActionHandler : CodeActivity
    {
        #region Custom Workflow Activity Parameters
        
        [RequiredArgument]
        [Input("Batch Process Record")]
        [ReferenceTarget("dyn_batchprocess")]
        public InArgument<EntityReference> BatchProcessReference { get; set; }

        #endregion

        #region Members

        ITracingService tracingService = null;
        IOrganizationService organizationService = null;

        #endregion

        #region Constants

        const string BATCH_PROCESS_ENTITY_NAME = "dyn_batchprocess";
        const int OCCURRENCE_HOURLY_ONE_TIME = 221490000;
        const int OCCURRENCE_HOURLY = 221490005;
        const int OCCURRENCE_DAILY = 221490004;
        const int OCCURRENCE_WEEKLY = 221490003;
        const int OCCURRENCE_MONTHLY = 221490002;
        const int OCCURRENCE_YEARLY = 221490001;
        const int STATUS_SCHEDULED = 1;
        const int STATUS_SUSPENDED = 221490000;
        const bool ACTIVATION_STATUS_FAILURE = false;
        const bool ACTIVATION_STATUS_SUCCESS = true;

        #endregion

        /// <summary>
        /// Executes the workflow activity
        /// </summary>
        /// <param name="executionContext">execution context</param>
        protected override void Execute(CodeActivityContext executionContext)
        {
            //init tracing service
            tracingService = InitTracingService(executionContext);

            //init organization service
            organizationService = InitOrganizationService(executionContext);

            Entity batchProcessRecord = null;

            try
            {
                //extract target batch process record id
                Guid batchProcessId = this.BatchProcessReference.Get(executionContext).Id;

                //retrieve batch process record 
                batchProcessRecord = RetrieveBatchProcessRecord(batchProcessId);

                //handle Process activation 
                HandleProcessActivation(batchProcessRecord);
            }
            catch (FaultException<OrganizationServiceFault> ex)
            {
                HandleError(ex, batchProcessRecord, tracingService);
            }
            catch (Exception ex)
            {
                HandleError(ex, batchProcessRecord, tracingService);
            }
        }

        #region Private Methods 

        /// <summary>
        /// Handle Action activation 
        /// </summary>
        /// <param name="executionContext"></param>
        private void HandleProcessActivation(Entity batchProcess)
        {
            ActivationDetails activationDetails = new ActivationDetails();
            Entity batchProcessActivation = null;
            string fetchXMLquery = string.Empty;

            //extract batch process status 
            OptionSetValue batchProcessStatus = (OptionSetValue)batchProcess["statuscode"];

            //operate only for scheduled status
            if (batchProcessStatus.Value == STATUS_SCHEDULED)
            {
                //set activation start date 
                activationDetails.ProcessStartDate = DateTime.Now;

                activationDetails.BatchProcess = batchProcess;

                //create Batch process activation record 
                batchProcessActivation = CreateBatchProcessActivationRecord(batchProcess.Id);
                activationDetails.BatchProcessActivation = batchProcessActivation;

                //retrieve action process record                     
                Guid workflowProcessId = ((Microsoft.Xrm.Sdk.EntityReference)(batchProcess["dyn_processid"])).Id;
                Entity actionProcessRecord = organizationService.Retrieve(
                    "workflow", workflowProcessId,
                    new ColumnSet("primaryentity", "uniquename"));

                //validate batch process details             
                activationDetails = ValidateBatchProcessDetails(activationDetails, actionProcessRecord);

                if (activationDetails.IsValidBatchProcess == true)
                {
                    //test for Target Records query existence
                    if (batchProcess.Contains("dyn_targetrecords"))
                    {
                        fetchXMLquery = batchProcess["dyn_targetrecords"].ToString();

                        //test for aggregative query 
                        if (IsAggregativeFetchXMLQuery(fetchXMLquery) == true)
                        {
                            //handle aggregative query
                            HandleAggregativeQuery(fetchXMLquery, actionProcessRecord);
                        }
                        else
                        {
                            //handle target records query
                            activationDetails.TotalTargetRecords = HandleTargetRecordsQuery(fetchXMLquery, actionProcessRecord);
                        }
                    }
                    else
                    {
                        //activate target Action with no target record
                        HandleExternalAction(actionProcessRecord);
                    }

                    //set next activation date 
                    if (batchProcess.Contains("dyn_nextactivation"))
                    {
                        SetBatchNextActivation(batchProcess);
                    }
                }

                FinalizeActivation(activationDetails);
            }
        }

        /// <summary>
        /// Returns true if Batch Process record is valid, else return false 
        /// alogn with invalidation reason 
        /// </summary>
        /// <param name="batchProcessRecord"></param>
        /// <returns></returns>
        private ActivationDetails ValidateBatchProcessDetails(ActivationDetails activationDetails, Entity actionProcess)
        {
            activationDetails.IsValidBatchProcess = true;

            if (actionProcess.Contains("dyn_targetrecords"))
            {
                string fetchXMLquery = activationDetails.BatchProcess["dyn_targetrecords"].ToString();

                //retrive workflow rule and test for fetchxml entity matching 
                bool actionWFFetchXMLMatch = IsActionWFMatchFetchXMLQuery(fetchXMLquery, actionProcess);

                //test workflow subject matches FetchXML Query subject 
                if (actionWFFetchXMLMatch == false)
                {
                    activationDetails.IsValidBatchProcess = false;
                    activationDetails.InvalidationReason = "Batch Process entity must match FetchXML query";
                }
            }

            return activationDetails;
        }

        /// <summary>
        /// Create a Batch Process Activation record
        /// </summary>
        /// <param name="batchProcessId">Parent Batch Process record id</param>
        /// <returns>Created Batch Process Activation record</returns>
        private Entity CreateBatchProcessActivationRecord(Guid batchProcessId)
        {
            Entity BatchProcessActivation = new Entity("dyn_batchprocessactivation");
            BatchProcessActivation["dyn_name"] = string.Format("Activation {0}", DateTime.Now.ToLocalTime());
            BatchProcessActivation["dyn_batchprocessid"] = new EntityReference("dyn_batchprocess", batchProcessId);
            BatchProcessActivation["dyn_activationstartdate"] = DateTime.Now;

            return BatchProcessActivation;
        }

        /// <summary>
        /// Set next batch activation time 
        /// </summary>
        /// <param name="batchProcess"></param>
        private void SetBatchNextActivation(Entity batchProcess)
        {
            OptionSetValue occurrenceMode = (OptionSetValue)batchProcess["dyn_activationfrequency"];
            DateTime nextActivationDate = (DateTime)batchProcess["dyn_nextactivation"];

            if (occurrenceMode.Value != OCCURRENCE_HOURLY_ONE_TIME)
            {
                switch (occurrenceMode.Value)
                {
                    case OCCURRENCE_HOURLY:
                        int hoursInterval = int.Parse(batchProcess["dyn_hoursinterval"].ToString());
                        batchProcess["dyn_nextactivation"] = nextActivationDate.AddHours(hoursInterval);
                        break;

                    case OCCURRENCE_DAILY:
                        batchProcess["dyn_nextactivation"] = nextActivationDate.AddDays(1);
                        break;

                    case OCCURRENCE_WEEKLY:
                        batchProcess["dyn_nextactivation"] = nextActivationDate.AddDays(7);
                        break;

                    case OCCURRENCE_MONTHLY:
                        batchProcess["dyn_nextactivation"] = nextActivationDate.AddMonths(1);
                        break;

                    case OCCURRENCE_YEARLY:
                        batchProcess["dyn_nextactivation"] = nextActivationDate.AddYears(1);
                        break;
                    default:
                        break;
                }

                //set status code to scheduled 
                batchProcess["statuscode"] = new OptionSetValue(STATUS_SCHEDULED);
            }
            else
            {
                //set status code to suspended 
                batchProcess["statuscode"] = new OptionSetValue(STATUS_SUSPENDED);
                batchProcess.Attributes.Remove("dyn_nextactivation");
            }
        }

        /// <summary>
        /// Retrieve target records
        /// </summary>
        /// <param name="fetchXMLQuery"></param>
        /// <param name="localContext"></param>
        /// <returns></returns>
        private EntityCollection RetrieveTargetRecords(string fetchXMLQuery)
        {
            EntityCollection result = null;

            // Convert the FetchXML into a query expression to override the 5000 records limit
            var conversionRequest = new FetchXmlToQueryExpressionRequest
            {
                FetchXml = fetchXMLQuery
            };

            var conversionResponse = (FetchXmlToQueryExpressionResponse)organizationService.Execute(conversionRequest);
            QueryExpression queryExpression = conversionResponse.Query;

            //execute fetchXML query
            result = organizationService.RetrieveMultiple(queryExpression);

            return result;
        }

        /// <summary>
        /// Apply workflow rule to target records
        /// </summary>
        /// <param name="targetRecords"></param>
        /// <param name="localContext"></param>
        private void ApplyWorkflowRule(EntityCollection targetRecords, Guid workflowProcessId)
        {
            //execute workflow rule for each target business record
            foreach (Entity businessEntity in targetRecords.Entities)
            {
                //Create an ExecuteWorkflow request
                ExecuteWorkflowRequest request = new ExecuteWorkflowRequest()
                {
                    WorkflowId = workflowProcessId,
                    EntityId = businessEntity.Id
                };

                ExecuteWorkflowResponse executeWorkflowResponse = (ExecuteWorkflowResponse)organizationService.Execute(request);
            }
        }

        /// <summary>
        /// Returns true if fetchXML query subject matches the specified Workflow rule subject 
        /// </summary>
        /// <param name="fetchXMLQuery"></param>
        /// <param name="?"></param>
        /// <returns></returns>
        private bool IsActionWFMatchFetchXMLQuery(string fetchXMLQuery, Entity actionProcessRecord)
        {
            bool result = false;
            string subjectEntity = string.Empty;

            //extract subject entity from fetchXML query 
            string pattern = "<entity\\s*name\\s*=\\s*('|\")(.*)('|\")\\s*>";

            Regex rgx = new Regex(pattern, RegexOptions.IgnoreCase);
            MatchCollection matches = rgx.Matches(fetchXMLQuery);

            if (matches.Count > 0)
            {
                subjectEntity = matches[0].Groups[1].Value;

                string wfRuleSubject = actionProcessRecord["primaryentity"].ToString();

                if (subjectEntity.ToLower() == wfRuleSubject.ToLower())
                {
                    result = true;
                }
            }

            return result;
        }

        /// <summary>
        /// Init tracing service 
        /// </summary>
        /// <param name="executionContext"></param>
        /// <returns></returns>
        private ITracingService InitTracingService(CodeActivityContext executionContext)
        {
            ITracingService result = null;

            // Create the tracing service
            result = executionContext.GetExtension<ITracingService>();

            return result;
        }

        /// <summary>
        /// Init organization service 
        /// </summary>
        /// <param name="executionContext"></param>
        /// <returns></returns>
        private IOrganizationService InitOrganizationService(CodeActivityContext executionContext)
        {
            IOrganizationService result = null;

            // extract context
            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
            result = serviceFactory.CreateOrganizationService(context.UserId);

            return result;
        }

        /// <summary>
        /// Retrieve batch process by id 
        /// </summary>
        /// <param name="batchProcessId"></param>
        /// <returns></returns>
        private Entity RetrieveBatchProcessRecord(Guid batchProcessId)
        {
            Entity result = null;

            result = organizationService.Retrieve(BATCH_PROCESS_ENTITY_NAME, batchProcessId,
                new ColumnSet(new string[] { 
                    "statuscode", 
                    "dyn_targetrecords", 
                    "dyn_processid", 
                    "dyn_nextactivation", 
                    "dyn_activationfrequency", 
                    "dyn_hoursinterval", 
                    "dyn_activationcounter" }));

            return result;
        }

        /// <summary>
        /// Document target records
        /// </summary>
        /// <param name="targetRecords"></param>
        private void DocumentTargetRecords(EntityCollection targetRecords, Guid batchProcessId)
        {
            StringBuilder sb = new StringBuilder();

            sb.AppendLine("Total Batch processed records: ");
            sb.Append(targetRecords.Entities.Count.ToString());

            foreach (Entity batchProcessRecord in targetRecords.Entities)
            {
                sb.AppendLine(batchProcessRecord.Id.ToString());
            }

            //create a Note record with target records ids and relate to batch process
            Entity annotation = new Entity("annotation");

            annotation["objectid"] = new EntityReference(BATCH_PROCESS_ENTITY_NAME, batchProcessId);
            annotation["subject"] = "Batch Process Details";
            annotation["notetext"] = sb.ToString();

            organizationService.Create(annotation);
        }

        /// <summary>
        /// Finalize activation 
        /// </summary>
        /// <param name="activationDetails"></param>
        private void FinalizeActivation(ActivationDetails activationDetails)
        {           
            //update Batch Process record
            activationDetails.BatchProcess["dyn_lastactivation"] = DateTime.Now.ToUniversalTime();
            activationDetails.BatchProcess["dyn_lastactivationcompletionstatus"] = ACTIVATION_STATUS_SUCCESS;
            activationDetails.BatchProcess["dyn_lastactivationerror"] = string.Empty;

            if (activationDetails.BatchProcess.Contains("dyn_activationcounter"))
            {
                int activationCounter = int.Parse(activationDetails.BatchProcess["dyn_activationcounter"].ToString());
                activationDetails.BatchProcess["dyn_activationcounter"] = ++activationCounter;
            }

            organizationService.Update(activationDetails.BatchProcess);

            //create Batch Process Activation record                                                   
            activationDetails.BatchProcessActivation["dyn_activationenddate"] = DateTime.Now;
            activationDetails.BatchProcessActivation["dyn_totalrecordprocessed"] = activationDetails.TotalTargetRecords;

            organizationService.Create(activationDetails.BatchProcessActivation);
        }

        /// <summary>
        /// Returns true if fetchXMLQuery is aggregative 
        /// </summary>
        /// <param name="fetchXMLQuery"></param>
        /// <returns></returns>
        private bool IsAggregativeFetchXMLQuery(string fetchXMLQuery)
        {
            bool result = false;

            //string pattern = "aggregate='true'";
            string pattern = "aggregate\\s*=\\s*('|\")true('|\")";

            Regex rgx = new Regex(pattern, RegexOptions.IgnoreCase);

            if (rgx.IsMatch(fetchXMLQuery) == true)
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Handle aggregative FetchXML query:
        /// 1. Execute query
        /// 2. Send result to target Action as input parameter
        /// </summary>
        /// <param name="batchProcessRecord"></param>
        private void HandleAggregativeQuery(string fetchXMLQuery, Entity action)
        {
            string aggregationResult = string.Empty;

            //execute query 
            EntityCollection queryResult = organizationService.RetrieveMultiple(new FetchExpression(fetchXMLQuery));

            //extract aggregation result value 
            aggregationResult = ExtractAggregationResults(fetchXMLQuery, queryResult);

            //execute action which is assumed to have at least one input parameter named aggregationResult            
            string solutionPrefix = RetrieveActionPrefix(action.Id);
            string actionSchemaName = string.Format("{0}_{1}", solutionPrefix, action["uniquename"].ToString());
            OrganizationRequest organizationRequest = new OrganizationRequest(actionSchemaName);
            organizationRequest["aggregationResult"] = aggregationResult;
            organizationService.Execute(organizationRequest);
        }

        /// <summary>
        /// Execute Action with no target records
        /// </summary>
        /// <param name="action"></param>
        private void HandleExternalAction(Entity action)
        {
            //execute action which is assumed to have no mandatory input parameters. Output parameters are ignored            
            string solutionPrefix = RetrieveActionPrefix(action.Id);
            string actionSchemaName = string.Format("{0}_{1}", solutionPrefix, action["uniquename"].ToString());
            OrganizationRequest organizationRequest = new OrganizationRequest(actionSchemaName);

            organizationService.Execute(organizationRequest);
        }

        /// <summary>
        /// Apply action to target records
        /// </summary>
        /// <param name="fetchXMLquery"></param>
        /// <param name="action"></param>
        private int HandleTargetRecordsQuery(string fetchXMLquery, Entity action)
        {
            int result = 0;

            //perform query 
            EntityCollection targetRecords = RetrieveTargetRecords(fetchXMLquery);

            //apply action to result records            
            ApplyWorkflowRule(targetRecords, action.Id);

            result = targetRecords.Entities.Count;

            return result;
        }

        /// <summary>
        /// Handle exception
        /// </summary>
        /// <param name="ex">Exception</param>
        /// <param name="batchProcess">Target batch process record</param>
        private void HandleError(Exception ex, Entity batchProcess, ITracingService tracingService)
        {
            string exceptionDetails = string.Format("Exception: {0}", string.Format("Exception message: {0},\nException Stack Tracee: {1}", ex.Message, ex.StackTrace));

            tracingService.Trace(exceptionDetails);

            //if batch process record was retrieved, update with error details 
            if (batchProcess != null)
            {
                //set last activation status and error description            
                batchProcess["dyn_lastactivation"] = DateTime.Now.ToUniversalTime();
                batchProcess["dyn_lastactivationcompletionstatus"] = ACTIVATION_STATUS_FAILURE;
                batchProcess["dyn_lastactivationerror"] = exceptionDetails;
                batchProcess["statuscode"] = new OptionSetValue(STATUS_SUSPENDED);                

                //update batch process record 
                organizationService.Update(batchProcess);
            }
        }

        /// <summary>
        /// Retrieve action's publisher prefix
        /// </summary>
        /// <param name="actionId"></param>
        /// <returns></returns>
        private string RetrieveActionPrefix(Guid actionId)
        {
            string result = string.Empty;

            string fetchXMLQuery = @"<fetch count='50' >
                                      <entity name='solutioncomponent' >
                                        <filter type='and' >
                                          <condition attribute='objectid' operator='eq' value='{0}' />
                                        </filter>
                                        <link-entity name='solution' from='solutionid' to='solutionid' link-type='inner' alias='solution'>
                                          <attribute name='uniquename' />
                                          <link-entity name='publisher' from='publisherid' to='publisherid' alias='publisher' >
                                            <attribute name='customizationprefix' />
                                          </link-entity>
                                        </link-entity>
                                      </entity>
                                    </fetch>";

            fetchXMLQuery = string.Format(fetchXMLQuery, actionId.ToString());

            EntityCollection queryResult = organizationService.RetrieveMultiple(new FetchExpression(fetchXMLQuery));

            //action is related to custom solution
            if (queryResult.Entities.Count > 2)
            {
                var solution = from e in queryResult.Entities
                               where
                                   (((Microsoft.Xrm.Sdk.AliasedValue)(e["solution.uniquename"])).Value.ToString() != "Default") &&
                                   (((Microsoft.Xrm.Sdk.AliasedValue)(e["solution.uniquename"])).Value.ToString() != "Active")
                               select new
                               {
                                   UniqueName = ((Microsoft.Xrm.Sdk.AliasedValue)(e["publisher.customizationprefix"])).Value.ToString()
                               };

                foreach (var s in solution)
                {
                    result = s.UniqueName;
                }
            }
            else //action is related to default solution
            {
                result = ((Microsoft.Xrm.Sdk.AliasedValue)(queryResult[0]["publisher.customizationprefix"])).Value.ToString();
            }

            return result;
        }

        /// <summary>
        /// Extract a string representing aggregation values from raw aggregation result
        /// </summary>
        /// <param name="rawAggregationResult"></param>
        /// <returns></returns>
        private string ExtractAggregationResults(string fetchXMLQuery, EntityCollection rawAggregationResult)
        {
            string result = string.Empty;

            //extract all aliases names from fetchXML query
            string pattern = "alias\\s*=\\s*('|\")([^('|\")]*)";
            Regex rgx = new Regex(pattern, RegexOptions.IgnoreCase);
            MatchCollection aliasMatches = rgx.Matches(fetchXMLQuery);

            //extract each alias value from result 
            foreach (Match match in aliasMatches)
            {
                //get aggregation raw result 
                string aliasName = match.Groups[2].Value;
                var aggregationRawVal = ((Microsoft.Xrm.Sdk.AliasedValue)(rawAggregationResult.Entities[0][aliasName])).Value;

                //get result type 
                Type aggregationValType = aggregationRawVal.GetType();

                //cast result to target type and extract value 
                switch (aggregationValType.Name)
                {
                    case "Money":
                        result += string.Format("{0} : {1}\n", aliasName, ((Money)aggregationRawVal).Value.ToString());
                        break;
                    case "Int32":
                        result += string.Format("{0} : {1}\n", aliasName, ((Int32)aggregationRawVal).ToString());
                        break;
                    default:
                        break;
                }
            }

            return result;
        }

        #endregion 
    }

    /// <summary>
    /// Data entitiy representing Batch Process Activation details 
    /// </summary>
    internal class ActivationDetails
    {
        private Entity batchProcess;
        private Entity batchProcessActivation;
        private DateTime processStartDate;
        private DateTime processEndDate;
        private bool isValidBatchProcess;
        private string invalidationReason;
        private Exception exception;
        private string targetRecordsQuery;
        private int totalTargetRecords;
        private bool isAggregationQuery;

        public string TargetRecordsQuery
        {
            get { return targetRecordsQuery; }
            set { targetRecordsQuery = value; }
        }

        public bool IsAggregationQuery
        {
            get { return isAggregationQuery; }
            set { isAggregationQuery = value; }
        }

        public int TotalTargetRecords
        {
            get { return totalTargetRecords; }
            set { totalTargetRecords = value; }
        }

        public Exception Exception
        {
            get { return exception; }
            set { exception = value; }
        }

        public bool IsValidBatchProcess
        {
            get { return isValidBatchProcess; }
            set { isValidBatchProcess = value; }
        }

        public string InvalidationReason
        {
            get { return invalidationReason; }
            set { invalidationReason = value; }
        }

        public DateTime ProcessEndDate
        {
            get { return processEndDate; }
            set { processEndDate = value; }
        }

        public DateTime ProcessStartDate
        {
            get { return processStartDate; }
            set { processStartDate = value; }
        }

        internal Entity BatchProcess
        {
            get { return batchProcess; }
            set { batchProcess = value; }
        }

        internal Entity BatchProcessActivation
        {
            get { return batchProcessActivation; }
            set { batchProcessActivation = value; }
        }
    }
}
